spb/datacenterindex
Public
HTML 53.9%
TypeScript 44.5%
JavaScript 0.6%
SQL 0.5%
1import type { Metadata } from "next";2import Link from "next/link";3import { notFound } from "next/navigation";4import { Breadcrumbs } from "@/components/entity/breadcrumbs";5import { JsonLd } from "@/components/entity/json-ld";6import { Badge } from "@/components/ui/badge";7import { cn } from "@/components/ui/cn";8import { Card, CardBody, CardHeader, Page } from "@/components/ui/card";9import { LinkTabs } from "@/components/ui/tabs";10import { CoverageBar } from "@/components/intelligence/shared";11import { CountryFlag } from "@/components/ui/country-flag";12import { EmptyState, OfflineNotice } from "@/components/ui/states";13import { RowLink, TBody, THead, Table, Td, Th, Tr } from "@/components/ui/table";14import { api } from "@/lib/api";15import { fmtDate, fmtDelta, fmtInt, fmtPct } from "@/lib/format";16import type { SearchParams } from "@/lib/sort";17import { routes } from "@/lib/routes";18import { NOT_FOUND_METADATA, absoluteUrl, pageMetadata } from "@/lib/seo";19import { SCOPE_LABEL, firstSentence, fmtRankValue, rowHref, secondaryLabel } from "../ranking-format";2021export const revalidate = 120;2223type Params = Promise<{ key: string }>;2425export async function generateMetadata({ params }: { params: Params }): Promise<Metadata> {26 const { key } = await params;27 const r = await api.ranking(key, { limit: 200 });28 if (!r.ok) return r.status === 404 || r.status === 400 ? NOT_FOUND_METADATA : { title: "Ranking" };29 const d = r.data;30 return pageMetadata({31 title: d.label,32 description: `Ranking of ${SCOPE_LABEL[d.scope].toLowerCase()} by ${d.label.toLowerCase()} — ${d.unit}. ${firstSentence(d.methodology)}`,33 path: routes.ranking(d.key),34 });35}3637export default async function RankingPage({ params, searchParams }: { params: Params; searchParams: Promise<SearchParams> }) {38 const [{ key }, sp] = await Promise.all([params, searchParams]);39 const mode = sp.mode === "all" ? "all" : "strict";40 const r = await api.ranking(key, { limit: 200, mode });41 if (!r.ok && (r.status === 404 || r.status === 400)) notFound();42 if (!r.ok) {43 return (44 <Page className="py-6">45 <Breadcrumbs items={[{ name: "Rankings", href: routes.rankings() }, { name: key, href: routes.ranking(key) }]} className="mb-4" />46 <OfflineNotice detail={r.error} />47 </Page>48 );49 }50 const d = r.data;51 const rows = d.rows ?? [];52 const showFlag = d.scope === "countries" || rows.some((x) => x.countryIso2);53 const hasSecondary = rows.some((x) => x.secondary !== null && x.secondary !== undefined);54 const hasCoverage = rows.some((x) => x.coverage !== null && x.coverage !== undefined);55 const hasDelta = rows.some((x) => x.delta !== null && x.delta !== undefined);56 const secLabel = secondaryLabel(d.methodology);5758 const jsonLd = {59 "@context": "https://schema.org",60 "@type": "ItemList",61 name: d.label,62 description: firstSentence(d.methodology),63 url: absoluteUrl(routes.ranking(d.key)),64 numberOfItems: rows.length,65 itemListOrder: "https://schema.org/ItemListOrderDescending",66 itemListElement: rows.slice(0, 100).map((row) => ({ "@type": "ListItem", position: row.rank, name: row.name, url: absoluteUrl(rowHref(row, d.scope)) })),67 };6869 return (70 <Page>71 <JsonLd data={jsonLd} />72 <Breadcrumbs items={[{ name: "Rankings", href: routes.rankings() }, { name: d.label, href: routes.ranking(d.key) }]} className="pt-4" />73 <header className="py-4 sm:py-6">74 <div className="label-caps mb-1.5">Ranking · {SCOPE_LABEL[d.scope]}</div>75 <h1 className="text-[22px] font-semibold leading-tight tracking-[-0.02em] text-ink sm:text-[28px]">{d.label}</h1>76 <div className="mt-2 flex flex-wrap items-center gap-2">77 <Badge tone="outline" mono>78 {d.unit}79 </Badge>80 <Badge tone="neutral">{SCOPE_LABEL[d.scope]}</Badge>81 <span className="figure text-[12px] text-ink-3">Computed {fmtDate(d.computedAt)}</span>82 {d.total > 0 && (83 <span className="figure text-[12px] text-ink-3">84 · {fmtInt(d.total)} {d.total === 1 ? "entity" : "entities"}85 </span>86 )}87 </div>88 {d.minCoverage !== undefined && d.minCoverage !== null && <p className="mt-2 max-w-2xl text-[12.5px] text-ink-3">Entities with fewer than 5 facilities or MW coverage below {fmtPct(d.minCoverage)} are excluded, not estimated.</p>}89 <p className="mt-1.5 max-w-2xl text-[12px] text-ink-4">Known / published figures only — entities without a figure are absent, not estimated.</p>90 </header>9192 {d.minCoverage !== undefined && d.minCoverage !== null && (93 <div className="mb-4">94 <LinkTabs95 items={[96 { href: routes.ranking(d.key), label: "Strict", active: mode === "strict", count: d.total },97 { href: `${routes.ranking(d.key)}?mode=all`, label: "All observed", active: mode === "all" },98 ]}99 />100 <p className="mt-2 text-[11.5px] text-ink-3">101 <strong className="font-medium text-ink-2">Strict</strong> = coverage gate applied (≥ 5 facilities, ≥ {fmtPct(d.minCoverage)} with a figure). <strong className="font-medium text-ink-2">All observed</strong> lists every entity with at least one published figure regardless of coverage; the coverage column shows how incomplete each row is. Nothing is estimated in either view.102 </p>103 </div>104 )}105106 {rows.length === 0 ? (107 <EmptyState108 title="No entity meets the coverage gate yet"109 description={110 d.minCoverage !== undefined && d.minCoverage !== null111 ? `This ranking only lists entities with at least 5 facilities and MW coverage of ${fmtPct(d.minCoverage)} or more. Nothing is estimated to fill the table; rows appear as primary sources publish capacity figures.`112 : "Rows appear once the worker has computed this ranking from indexed facilities. Nothing is estimated to fill the table."113 }114 action={115 <Link href={routes.rankings()} className="text-[13px] text-ink underline decoration-hair-2 hover:text-accent">116 All rankings117 </Link>118 }119 />120 ) : (121 <Table dense mobileCollapse={{ total: rows.length, label: "rows" }}>122 <THead>123 <tr>124 <Th numeric className="w-12">125 #126 </Th>127 <Th>{d.scope === "countries" ? "Country" : d.scope === "metros" ? "Market" : d.scope === "operators" ? "Operator" : "Facility"}</Th>128 <Th numeric>{d.unit}</Th>129 {hasSecondary && (130 <Th numeric className="hidden md:table-cell">131 {secLabel}132 </Th>133 )}134 {hasCoverage && <Th className="w-[140px]">Coverage</Th>}135 {hasDelta && (136 <Th numeric className="hidden sm:table-cell">137 Δ rank138 </Th>139 )}140 </tr>141 </THead>142 <TBody>143 {rows.map((row) => (144 <Tr key={row.id}>145 <Td label="Rank" numeric className="text-ink-3">146 {row.rank}147 </Td>148 <Td primary>149 <span className="inline-flex items-center gap-2">150 {showFlag && row.countryIso2 && <CountryFlag iso2={row.countryIso2} size={13} />}151 <RowLink href={rowHref(row, d.scope)}>{row.name}</RowLink>152 </span>153 </Td>154 <Td label={d.unit} numeric lead>155 {fmtRankValue(row.value, d.unit)}156 </Td>157 {hasSecondary && (158 <Td label={secLabel} numeric className="hidden text-ink-3 md:table-cell">159 {row.secondary === null || row.secondary === undefined ? "—" : fmtInt(row.secondary)}160 </Td>161 )}162 {hasCoverage && (163 <Td label="Coverage">164 {row.coverage === null || row.coverage === undefined ? (165 <span className="text-ink-4">—</span>166 ) : (167 <CoverageBar value={row.coverage} width="w-full sm:w-[120px]" label="Share of facilities with a MW figure" />168 )}169 </Td>170 )}171 {hasDelta && (172 <Td label="Δ rank" numeric className="hidden sm:table-cell">173 {row.delta === null || row.delta === undefined ? <span className="text-ink-4">new</span> : <span className={cn(row.delta > 0 ? "text-ok" : row.delta < 0 ? "text-danger" : "text-ink-3")}>{fmtDelta(row.delta)}</span>}174 </Td>175 )}176 </Tr>177 ))}178 </TBody>179 </Table>180 )}181182 <div className="mt-8 grid gap-4 md:grid-cols-[minmax(0,2fr)_minmax(0,1fr)]">183 <Card>184 <CardHeader level={2} title="Methodology" description={`Unit: ${d.unit} · scope: ${SCOPE_LABEL[d.scope].toLowerCase()}`} />185 <CardBody>186 <p className="text-[13.5px] leading-relaxed text-ink-2">{d.methodology}</p>187 </CardBody>188 </Card>189 <Card>190 <CardHeader level={2} title="How to read this" />191 <CardBody className="text-[12.5px] leading-relaxed text-ink-2">192 <p>Values are lower bounds computed from published or filed figures only. Coverage is the share of an entity's facilities carrying any MW figure; a low coverage means the value understates reality.</p>193 <p className="mt-2">Δ rank compares with the previous computation of the same ranking; “new” marks an entity that was not ranked before.</p>194 <p className="mt-3">195 <Link href={routes.rankings()} className="text-ink underline decoration-hair-2 hover:text-accent">196 All rankings197 </Link>198 <span className="text-ink-4"> · </span>199 <Link href={`${routes.methodology()}#rankings`} className="text-ink underline decoration-hair-2 hover:text-accent">200 Methodology201 </Link>202 </p>203 </CardBody>204 </Card>205 </div>206 </Page>207 );208}209